package com.szeldon.declatest.declaration;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import com.szeldon.declatest.declaration.endpoints.EndpointDeclaration;
import com.szeldon.declatest.declaration.props.PropertyFileDeclaration;
import com.szeldon.declatest.declaration.reader.exceptions.TestSuiteFileException;
import com.szeldon.declatest.declaration.scenario.ScenarioDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.CheckpointPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.DoScenarioStepPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ReturnPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ScenarioPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ScenarioPartsHelper;
import com.szeldon.declatest.declaration.scenario.parts.ScenarioPartsTypeEnum;
import com.szeldon.declatest.declaration.settings.SettingsDeclaration;
import com.szeldon.declatest.declaration.steps.ScenarioStepDeclaration;

/**
 * Class holding data from files with declaration of tests.
 * 
 * @author szeldon
 * 
 */
public class TestsSuiteDeclaration {

    /** Map of attribute types and their values. */
    private Map<TestsSuiteAttributeTypeEnum, String> attributeTypeValueMap = new HashMap<TestsSuiteAttributeTypeEnum, String>();

    /** List of property files. */
    private List<PropertyFileDeclaration> propertyFilesList = new ArrayList<PropertyFileDeclaration>();

    /** List of settings. */
    private Map<String, SettingsDeclaration> settingsMap = new HashMap<String, SettingsDeclaration>();

    /** List of enpoints. */
    private Map<String, EndpointDeclaration> endpointsMap = new HashMap<String, EndpointDeclaration>();

    /** List of steps. */
    private Map<String, ScenarioStepDeclaration> stepsMap = new HashMap<String, ScenarioStepDeclaration>();

    /** List of scenarios. */
    private List<ScenarioDeclaration> scenariosList = new ArrayList<ScenarioDeclaration>();

    /**
     * Empty constructor.
     */
    public TestsSuiteDeclaration() {
    }

    /**
     * Adds attribute of the whole TestSuite. Replaces before-added values for same names.
     * 
     * @param attributeType
     *            Type of attribute.
     * @param attributeValue
     *            Value of attribute.
     * @throws IllegalArgumentException
     *             Thrown when given attributeType isn't supported or value isn't proper.
     */
    public void addTestSuiteAttribute(String attributeType, String attributeValue) throws IllegalArgumentException {

        TestsSuiteAttributeTypeEnum typeEnum = mapAttributeType(attributeType);

        if (typeEnum == null) {
            throw new IllegalArgumentException("TestsSuite's attribute type \"" + attributeType + "\" isn't supported.");
        }

        verifyValue(typeEnum, attributeValue);

        attributeTypeValueMap.put(typeEnum, attributeValue);
    }

    /**
     * Maps given type from string to enum.
     * 
     * @param attributeType
     * @return
     */
    private TestsSuiteAttributeTypeEnum mapAttributeType(String attributeType) {
        TestsSuiteAttributeTypeEnum typeEnum = null;

        if (attributeType.equals(TestsSuiteAttributeTypeEnum.TEST_SUITE_ID.getText())) {
            typeEnum = TestsSuiteAttributeTypeEnum.TEST_SUITE_ID;
        } else if (attributeType.equals(TestsSuiteAttributeTypeEnum.TEST_SUITE_DESCRIPTION.getText())) {
            typeEnum = TestsSuiteAttributeTypeEnum.TEST_SUITE_DESCRIPTION;
        }

        return typeEnum;
    }

    /**
     * Verifies if given value fits given type of attribute.
     * 
     * @param typeEnum
     * @param attributeValue
     * @throws IllegalArgumentException
     *             Thrown when given attribute value isn't proper.
     */
    private void verifyValue(TestsSuiteAttributeTypeEnum typeEnum, String attributeValue)
            throws IllegalArgumentException {
        switch (typeEnum) {
        case TEST_SUITE_ID: {
            if (attributeValue.contains(" ")) {
                throw new IllegalArgumentException("ID of TestsSuite can't contain spaces. Currently it's \""
                        + attributeValue + "\".");
            }
        }
        case TEST_SUITE_DESCRIPTION: {

        }
        }
    }

    /**
     * Returns ID of this TestSuite declaration.
     * 
     * @return
     */
    public String getId() {
        return attributeTypeValueMap.get(TestsSuiteAttributeTypeEnum.TEST_SUITE_ID);
    }

    /**
     * Returns description of this TestsSuite declaration.
     * 
     * @return
     */
    public String getDescription() {
        return attributeTypeValueMap.get(TestsSuiteAttributeTypeEnum.TEST_SUITE_DESCRIPTION);
    }

    /**
     * Checks if this declaration is proper. Throws exception if isn't proper. Things that are checked:
     * <ul>
     * <li>IDs of property files, settings, endpoints, scenario steps, scenarios and scenario parts must be unique among
     * each other (e.g. IDs of settings mustn't have the same value as IDs of property file). IDs are global in test
     * suite.</li>
     * <li>Endpoint must contain settingsId that exists among IDs of Settings elements.</li>
     * <li>ScenarioSteps must contain endpointId that exists among IDs of Endpoint elements.</li>
     * <li></li>
     * <li>TODO Some scenario parts can't be followed by any part (
     * {@link ScenarioPartsHelper#canBeOneAfterAnother(ScenarioPartsTypeEnum, ScenarioPartsTypeEnum)}</li>
     * <li>TODO Return must use checkpointId of Checkpoint that was in the same part of the tree.</li>
     * </ul>
     * 
     * @return
     */
    public void check() throws TestSuiteFileException {

        HashSet<String> idsSet = new HashSet<String>();
        HashSet<String> settingsIds = new HashSet<String>();
        HashSet<String> endpointIds = new HashSet<String>();
        HashSet<String> stepIds = new HashSet<String>();
        HashSet<String> scenarioIds = new HashSet<String>();
        HashSet<String> checkpointIds = new HashSet<String>();

        for (SettingsDeclaration currentSettings : settingsMap.values()) {
            String id = currentSettings.getId();
            settingsIds.add(id);
            if (!idsSet.add(id)) {
                throw new TestSuiteFileException(id + " ID is a duplicate in settings.");
            }
        }
        for (EndpointDeclaration currentEndpoint : endpointsMap.values()) {
            String id = currentEndpoint.getId();
            endpointIds.add(id);
            if (!idsSet.add(id)) {
                throw new TestSuiteFileException(id + " ID is a duplicate in endpoints.");
            }
            if (!settingsIds.contains(currentEndpoint.getSettingsId())) {
                throw new TestSuiteFileException("SettingsId: " + currentEndpoint.getSettingsId() + " from endpoint: "
                        + currentEndpoint.getId() + " isn't on Settings list.");
            }
        }
        for (ScenarioStepDeclaration currentStep : stepsMap.values()) {
            String id = currentStep.getId();
            stepIds.add(id);
            if (!idsSet.add(id)) {
                throw new TestSuiteFileException(id + " ID is a duplicate in scenario steps.");
            }
            if (!endpointIds.contains(currentStep.getEndpointId())) {
                throw new TestSuiteFileException("EndpointId: " + currentStep.getEndpointId() + " from ScenarioStep: "
                        + currentStep.getId() + " isn't on Endpoints list.");
            }
        }
        for (ScenarioDeclaration currentScenario : scenariosList) {
            String id = currentScenario.getId();
            scenarioIds.add(id);
            if (!idsSet.add(id)) {
                throw new TestSuiteFileException(id + " ID is a duplicate in scenarios.");
            }

            verifyScenarioForDuplicateAndMissingIds(idsSet, currentScenario, settingsIds, endpointIds, stepIds,
                    scenarioIds, checkpointIds);
        }
    }

    /**
     * Returns true if scenario doesn't contain duplicate IDs and those IDs are proper (they use declared IDs).
     * 
     * @param idsSet
     *            Set of all IDs of Settings, Endpoints, ScenarioSteps.
     * @param scenario
     * @param scenarioIds
     * @param stepIds
     * @param endpointIds
     * @param settingsIds
     * @param checkpointIds
     * @throws TestSuiteFileException
     */
    private void verifyScenarioForDuplicateAndMissingIds(HashSet<String> idsSet, ScenarioDeclaration scenario,
            HashSet<String> settingsIds, HashSet<String> endpointIds, HashSet<String> stepIds,
            HashSet<String> scenarioIds, HashSet<String> checkpointIds) throws TestSuiteFileException {

        List<ScenarioPartDeclaration> subparts = scenario.getPartsList();
        for (ScenarioPartDeclaration currentPart : subparts) {

            verifyPartIdsForDuplicatesAndMissing(currentPart, idsSet, settingsIds, endpointIds, stepIds, scenarioIds,
                    checkpointIds);

        }

    }

    /**
     * Goes recursively by all the subparts of given part looking for missing and duplicate IDs.
     * 
     * @param part
     * @param idsSet
     * @param settingsIds
     * @param endpointIds
     * @param stepIds
     * @param scenarioIds
     * @param checkpointIds
     * @throws TestSuiteFileException
     */
    private void verifyPartIdsForDuplicatesAndMissing(ScenarioPartDeclaration part, HashSet<String> idsSet,
            HashSet<String> settingsIds, HashSet<String> endpointIds, HashSet<String> stepIds,
            HashSet<String> scenarioIds, HashSet<String> checkpointIds) throws TestSuiteFileException {

        for (ScenarioPartDeclaration currentSubpart : part.getSubpartsList()) {
            if (currentSubpart instanceof CheckpointPartDeclaration) {
                String currentId = ((CheckpointPartDeclaration) currentSubpart).getId();
                if (!idsSet.add(currentId)) {
                    throw new TestSuiteFileException(currentId + " is a duplicate in parts.");
                }
                checkpointIds.add(currentId);
            }
            if (currentSubpart instanceof DoScenarioStepPartDeclaration) {
                String stepId = ((DoScenarioStepPartDeclaration) currentSubpart).getStepId();
                if (!stepIds.contains(stepId)) {
                    throw new TestSuiteFileException("ScenarioStep with ID: " + stepId + " doesn't exist.");
                }
            }
            if (currentSubpart instanceof ReturnPartDeclaration) {
                String checkpointId = ((ReturnPartDeclaration) currentSubpart).getCheckpointId();
                if (!checkpointIds.contains(checkpointId)) {
                    throw new TestSuiteFileException("Checkpoint with ID: " + checkpointId + " doesn't exist.");
                }
            }
            verifyPartIdsForDuplicatesAndMissing(currentSubpart, idsSet, settingsIds, endpointIds, stepIds,
                    scenarioIds, checkpointIds);
        }
    }

    /**
     * Adds property file to this test suite.
     * 
     * @param propertyFile
     */
    public void addPropertyFile(PropertyFileDeclaration propertyFile) {
        propertyFilesList.add(propertyFile);
    }

    /**
     * Returns list of property files declarations.
     * 
     * @return
     */
    public List<PropertyFileDeclaration> getPropertyFilesList() {
        return propertyFilesList;
    }

    public Collection<SettingsDeclaration> getSettings() {
        return settingsMap.values();
    }

    public Collection<EndpointDeclaration> getEndpoints() {
        return endpointsMap.values();
    }

    public Collection<ScenarioStepDeclaration> getSteps() {
        return stepsMap.values();
    }

    public List<ScenarioDeclaration> getScenarios() {
        return scenariosList;
    }

    /**
     * Adds settings part to this scenario.
     * 
     * @param settingsPart
     */
    public void addSettings(SettingsDeclaration settingsPart) {
        settingsMap.put(settingsPart.getId(), settingsPart);
    }

    /**
     * Adds given endpoint part to this scenario.
     * 
     * @param endpointPart
     */
    public void addEndpoint(EndpointDeclaration endpointPart) {
        endpointsMap.put(endpointPart.getId(), endpointPart);
    }

    /**
     * Adds given scenario step to this scenario.
     * 
     * @param scenarioStepDeclaration
     */
    public void addScenarioStep(ScenarioStepDeclaration scenarioStepDeclaration) {
        stepsMap.put(scenarioStepDeclaration.getId(), scenarioStepDeclaration);
    }

    /**
     * Adds scenario to this test suite.
     * 
     * @param scenario
     */
    public void addScenarioStep(ScenarioDeclaration scenario) {
        scenariosList.add(scenario);
    }

    /**
     * Gets a scenario step by its id. If there's no such a step, null is returned.
     * 
     * @param stepId
     * @return
     */
    public ScenarioStepDeclaration getStepById(String stepId) {
        return stepsMap.get(stepId);
    }

    /**
     * Returns endpoint identified by its id. If there's no such an endpoint, null is returned.
     * 
     * @param endpointId
     */
    public EndpointDeclaration getEndpointById(String endpointId) {
        return endpointsMap.get(endpointId);
    }

    /**
     * Returns settings identified by id. If there's no such settings object, null is returned.
     * 
     * @param settingsId
     * @return
     */
    public SettingsDeclaration getSettingsById(String settingsId) {
        return settingsMap.get(settingsId);
    }

}
